package org.jslaughter.component.ai;

import java.util.Random;

import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.World;
import org.jslaughter.event.eventtype.AggressionEvent;
import org.jslaughter.event.eventtype.DamageEvent;
import org.jslaughter.event.eventtype.DamageEvent.DAMAGE_TYPE;
import org.jslaughter.event.eventtype.DamageListener;
import org.jslaughter.event.eventtype.PullEvent;
import org.jslaughter.event.eventtype.PullListener;
import org.nvframe.component.AbstractComponent;
import org.nvframe.component.ai.Behaviour.BEHAVIOUR_TYPE;
import org.nvframe.component.physics.state.PhysicsPosition;
import org.nvframe.component.state.Position;
import org.nvframe.entity.Entity;
import org.nvframe.event.EventService;
import org.nvframe.event.eventtype.UpdateEvent;
import org.nvframe.event.eventtype.UpdateListener;
import org.nvframe.exception.NVFrameException;
import org.nvframe.manager.RegisterManager;
import org.nvframe.util.NVMathUtils;
import org.nvframe.util.settings.SettingsObj;

/**
 * 
 * @author Nik Van Looy
 */
public class ZombieAI extends AbstractComponent implements UpdateListener, DamageListener, PullListener {

	private float stepSpeed;
	private float rageSpeedPct;
	private float rotSpeed;
	private float lockOnRadius;
	private float lockOffRadius;
	private int checkTargetMs;
	private int minChangeDirectionMs;
	private int maxChangeDirectionMs;
	private int minPauzeMs;
	private int maxPauzeMs;
	
	private int currPauzeMs;
	private int currCheckTargetMs;
	private int currChangeDirectionMs;
	private float destDirection;
	private Entity target;
	private boolean lockedOn = false;
	// this is set to true when a zombie is damaged by a player => permanent lockOn
	private boolean retaliationMode = false;
	private World world;
	
	private DAMAGE_TYPE damageType;

	public ZombieAI(String id, Entity owner, SettingsObj settings) throws NVFrameException {
		super(id, owner);
		
		stepSpeed = settings.getFloat("stepSpeed");
		rageSpeedPct = settings.getFloat("rageSpeedPct", 0);
		rotSpeed = settings.getFloat("rotSpeed");
		lockOnRadius = settings.getFloat("lockOnRadius", 200f);
		lockOffRadius = settings.getFloat("lockOffRadius", 380f);
		minChangeDirectionMs = settings.getInt("minChangeDirectionMs", 2000);
		maxChangeDirectionMs = settings.getInt("maxChangeDirectionMs", 3000);
		minPauzeMs = settings.getInt("minPauzeMs", 2000);
		maxPauzeMs = settings.getInt("maxPauzeMs", 4000);
		currCheckTargetMs = checkTargetMs = settings.getInt("checkTargetMs", 1000);

		damageType = DAMAGE_TYPE.valueOf(settings.getString("damageType", 
				DAMAGE_TYPE.MELEE.toString()));
	}

	public void init() {
		Position pos = (Position) owner.getComponent(Position.class);
		destDirection = pos.getDirection();
		EventService.getInstance().addEventListener(this);
		
		world = (World) RegisterManager.getInstance().getOption("world");
	}
	
	/**
	 * Calculates the degrees of angle he must move
	 * depending on delta and rotationSpeed
	 * 
	 * @param currentDir
	 * @param destDir
	 * @param delta
	 * @return
	 */
	private float calculateDirection(float currentDir, float destDir, int delta) {
		boolean clockwise = true;

		currentDir = currentDir % 360 < 0 ? 360 - Math.abs(currentDir % 360) : Math.abs(currentDir % 360);
		destDir = destDir % 360 < 0 ? 360 - Math.abs(destDir % 360) : Math.abs(destDir % 360);
		
		if(Math.abs(currentDir - destDir) <= rotSpeed)
			return currentDir;
		
		clockwise = destDir - currentDir < 0 ? false : true;
		
		if(Math.abs(destDir - currentDir) > 180)
			clockwise = !clockwise;
		
		if(clockwise)
			return currentDir + rotSpeed * delta;
		else
			return currentDir - rotSpeed * delta;
	}
	
	private Entity getBestTarget() {

		Position positionComp = (Position) owner.getComponent(Position.class);
		BehaviourImpl behaviourComp = (BehaviourImpl) owner.getComponent(BehaviourImpl.class);
		
		// when in lockedOnOverride => skip bodyscan
		if(retaliationMode && target != null && target.isEnabled())
			return target;
		
		retaliationMode = false;
		float minDistance = 0;
		Entity tempTarget = null;
		
		// find the closest body in the attackradius
		for (Body body = world.getBodyList(); body.getNext() != null; body = body.getNext()) {
			if (body.isBullet())
				continue;
			
			// check behaviour between the two entities
			Entity entity = (Entity) body.getUserData();

			if(behaviourComp.getBehaviourTowards(entity) != BEHAVIOUR_TYPE.HOSTILE)
				continue;
			
			if(!behaviourComp.canDoDamageTo(entity, damageType))
				continue;
			
			// distance too far => target aint good
			Position entityPositionComp = (Position) entity.getComponent(Position.class);
			float distance = positionComp.getXY().distance(entityPositionComp.getXY());
			
			// stop chasing target when exeeding lockOffRadius
			if(entity == target && distance >= lockOffRadius)
				continue;
			
			// when not in lockedOn the lockOnRadius counts
			if(target == null && distance >= lockOnRadius)
				continue;
				
			if(minDistance == 0)
				minDistance = distance;
			
			// save a reference to the closest target so far
			if(distance > minDistance)
				continue;
			
			tempTarget = entity;
			lockedOn = true;
		}
		
		if(target != tempTarget)
			EventService.getInstance().fireEvent(new AggressionEvent(owner, tempTarget), owner);
		
		return tempTarget;
	}

	@Override
	public void onUpdate(UpdateEvent event) {
		PhysicsPosition positionComp = (PhysicsPosition) owner.getComponent(PhysicsPosition.class);

		int delta = event.getDelta();
		
		if(currChangeDirectionMs >= 0)
			currChangeDirectionMs -= delta;
		if(currCheckTargetMs >= 0)
			currCheckTargetMs -= delta;
		if(currPauzeMs >= 0)
			currPauzeMs -= delta;
		
		// check for other targets?
		if(currCheckTargetMs <= 0 && !retaliationMode) {
			currCheckTargetMs = checkTargetMs;
			target = getBestTarget();
		}
		
		// calculate direction
		if(currChangeDirectionMs <= 0 || lockedOn) {
			currChangeDirectionMs = NVMathUtils.getRandomNumberBetween(minChangeDirectionMs, 
					maxChangeDirectionMs);
			
			if(lockedOn && target != null && target.isEnabled()) {
				Position targetPositionComp = (Position) target.getComponent(Position.class);
				
				destDirection = (float) Math.toDegrees(Math.atan2(targetPositionComp.getY() - positionComp.getY(), 
						targetPositionComp.getX() - positionComp.getX()));
			}
			else {				
				lockedOn = false;
				Random generator = new Random();
				
				if(generator.nextInt(2) == 0)
					destDirection += (float) (generator.nextFloat() * 90) + 1;
				else
					destDirection -= (float) (generator.nextFloat() * 90) + 1;
				
				// random pause
				if(generator.nextInt(100) <= 5)
					currPauzeMs = NVMathUtils.getRandomNumberBetween(minPauzeMs, maxPauzeMs);;
			}
		}

		// set velocity so owner moves to targetEntity's position
		float stepSpeed = lockedOn ? this.stepSpeed * rageSpeedPct : this.stepSpeed;
		
		if(currPauzeMs <= 0)
			positionComp.applyImpulse((float) (Math.cos(Math.toRadians(positionComp.getDirection())) * stepSpeed * delta), 
					(float) (Math.sin(Math.toRadians(positionComp.getDirection())) * stepSpeed * delta));
		
		// change direction of owner's render
		positionComp.setDirection(calculateDirection(positionComp.getDirection(), destDirection, delta));
	}

	@Override
	public void damageReceived(DamageEvent event) {
		if(event.getTarget() != owner)
			return;
		
		BehaviourImpl behaviourComp = (BehaviourImpl) owner.getComponent(BehaviourImpl.class);
		Entity target = event.getSource().getRoot();
		
		if(!behaviourComp.canDoDamageTo(target, DAMAGE_TYPE.MELEE))
			return;
		
		if(behaviourComp.getBehaviourTowards(target) != BEHAVIOUR_TYPE.HOSTILE)
			return;
		
		this.target = target;
		lockedOn = retaliationMode = true;
		EventService.getInstance().fireEvent(new AggressionEvent(owner, target), owner);
	}

	@Override
	public void onPulled(PullEvent event) {
		if(lockedOn)
			return;
		
		target = event.getTarget();
		lockedOn = true;
		retaliationMode = false;
	}

}